#include "ModelMD5.h"
#include "../Logger.h"
#include <string.h>

#include "../SOIL/SOIL.h"

#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp>
namespace fs = boost::filesystem;

#define GLM_SWIZZLE_XYZW
#include "../glm/glm.hpp"
#include "../glm/gtc/type_ptr.hpp"
#include "../glm/gtc/swizzle.hpp"
#include "../glm/gtc/quaternion.hpp"
#include "../glm/gtx/vector_angle.hpp"
#include "../glm/gtx/compatibility.hpp"
#include "../glm/gtx/matrix_operation.hpp"
#include "../glm/gtx/transform.hpp"
#include "../glm/gtx/quaternion.hpp"
#include "../glm/gtx/euler_angles.hpp"

_ModelMD5::_ModelMD5():iMD5Version(-1),bHasAnimation(false),localToWorldMatrix(1){}
_ModelMD5::~_ModelMD5(){
	
}
void _ModelMD5::computeQuatW( glm::quat& quat ){
    float t = 1.0f - ( quat.x * quat.x ) - ( quat.y * quat.y ) - ( quat.z * quat.z );
    if ( t < 0.0f )
    {
        quat.w = 0.0f;
    }
    else
    {
        quat.w = -sqrtf(t);
    }
}
bool _ModelMD5::load(const char* filename){
	Str fn;
	fn << filename << "mesh";
    if ( !fs::exists(!fn) )
    {
        errLog.print(Str()<<"MD5Model::LoadModel: Failed to find file: "<<fn);
        return false;
    }
	
    fs::path filePath = !fn;
    // store the parent path used for loading images relative to this file.
    fs::path parent_path = filePath.parent_path();
	
    std::string param;
    std::string junk;   // Read junk from the file
	
    fs::ifstream file(!fn);
    int fileLength = getFileLength(file);
    if( fileLength == 0 )
	{
		errLog.print(Str()<<"Model "<<fn<<" is unreadable");
		return false;
	}
	joints.clear();
	meshes.clear();
	
	file >> param;
	
	while ( !file.eof() )
	{
		if ( param == "MD5Version" )
		{
			file >> iMD5Version;
			if ( iMD5Version != 10 )
			{
				errLog.print(Str()<<"Model "<<fn<<" has an unrecognized verison of "<<iMD5Version<<" (only version 10 is supported)");
			}
		}
		else if ( param == "commandline" )
		{
			ignoreLine(file, fileLength ); // Ignore the contents of the line
		}
		else if ( param == "numJoints" )
		{
			file >> iNumJoints;
			joints.reserve(iNumJoints);
		}
		else if ( param == "numMeshes" )
		{
			file >> iNumMeshes;
			meshes.reserve(iNumMeshes);
		}
		else if ( param == "joints" )
		{
			Joint joint;
			file >> junk; // Read the '{' character
			for ( int i = 0; i < iNumJoints; i++ )
			{
				file
				>> joint.name
				>> joint.parentID
				>> junk
				>> joint.pos.x
				>> joint.pos.y
				>> joint.pos.z
				>> junk
				>> junk
				>> joint.orient.x
				>> joint.orient.y
				>> joint.orient.z
				>> junk;
				
				removeQuotes(joint.name);
				computeQuatW(joint.orient);
				
				joints.push_back(joint);
				// Ignore everything else on the line up to the end-of-line character.
				ignoreLine(file, fileLength);
			}
			file >> junk; // Read the '}' character
		}
		else if (param == "mesh" )
		{
			Mesh mesh;
			int numVerts, numTris, numWeights;
			
			file >> junk; // Read the '{' character
			file >> param;
			while (param != "}")  // Read until we get to the '}' character
			{
				if (param == "shader")
				{
					file >> mesh.shader;
					removeQuotes( mesh.shader );//mesh.shader -> shaderName
					fs::path shaderPath(mesh.shader);
					fs::path texturePath;
					if (shaderPath.has_parent_path())
					{
						texturePath = shaderPath;
					}
					else
					{
						texturePath = parent_path / shaderPath;
					}
					
					if (!texturePath.has_extension())
					{
						texturePath.replace_extension(".tga");
					}
					
					mesh.texID = SOIL_load_OGL_texture( texturePath.string().c_str(), SOIL_LOAD_AUTO, SOIL_CREATE_NEW_ID, SOIL_FLAG_MIPMAPS );
					
					file.ignore(fileLength, '\n' ); // Ignore everything else on the line
				}
				else if (param == "numverts")
				{
					file >> numVerts;               // Read in the vertices
					ignoreLine(file, fileLength);
					for (int i = 0; i < numVerts; i++)
					{
						Vertex vert;
						
						file
						>> junk
						>> junk
						>> junk		// vert vertIndex (
						>> vert.tex0.x
						>> vert.tex0.y
						>> junk		//  s t )
						>> vert.startWeight
						>> vert.weightCount;
						
						ignoreLine(file, fileLength);
						
						mesh.verts.push_back(vert);
						mesh.tex2DBuffer.push_back(vert.tex0);
					}
				}
				else if (param == "numtris")
				{
					file >> numTris;
					ignoreLine(file, fileLength);
					for (int i = 0; i < numTris; i++)
					{
						Triangle tri;
						file
						>> junk
						>> junk
						>> tri.indices[0]
						>> tri.indices[1]
						>> tri.indices[2];
						
						ignoreLine(file, fileLength);
						
						mesh.tris.push_back(tri);
						mesh.indexBuffer.push_back( (GLuint)tri.indices[0] );
						mesh.indexBuffer.push_back( (GLuint)tri.indices[1] );
						mesh.indexBuffer.push_back( (GLuint)tri.indices[2] );
					}
				}
				else if (param == "numweights")
				{
					file >> numWeights;
					ignoreLine( file, fileLength );
					for ( int i = 0; i < numWeights; ++i )
					{
						Weight weight;
						file
						>> junk
						>> junk
						>> weight.jointID
						>> weight.bias
						>> junk
						>> weight.pos.x
						>> weight.pos.y
						>> weight.pos.z
						>> junk;
						
						ignoreLine( file, fileLength );
						mesh.weights.push_back(weight);
					}
				}
				else
                {
                    ignoreLine(file, fileLength);
                }
				
                file >> param;
            }
			
            prepareMesh(mesh);
            prepareNormals(mesh);
			
            meshes.push_back(mesh);
			
        }
		
        file >> param;
    }
	
    if ((int)joints.size() != iNumJoints)
	{
		errLog.print(Str()<<"Model \""<<fn<<"\" has corrupted joints");
	}
    if ((int)meshes.size() != iNumMeshes )
	{
		errLog.print(Str()<<"Model \""<<fn<<"\" has corrupted mesh");
	}
	
	fn.clear();
	fn << filename << "anim";
	
	bHasAnimation = animation.load(!fn);
	if (!bHasAnimation){mainLog.print("NOPE");}
	else {animation.update(1);mainLog.print("YUP");}
	
    return true;
}
bool _ModelMD5::prepareMesh(Mesh &mesh){
    mesh.positionBuffer.clear();
    mesh.tex2DBuffer.clear();
	
    // Compute vertex positions
    for (unsigned int i = 0; i < mesh.verts.size(); i++)
    {
        glm::vec3 finalPos(0);
        Vertex& vert = mesh.verts[i];
		
        vert.pos = glm::vec3(0);
        vert.normal = glm::vec3(0);
		
        // Sum the position of the weights
        for (int j = 0; j < vert.weightCount; j++)
        {
            Weight& weight = mesh.weights[vert.startWeight + j];
            Joint& joint = joints[weight.jointID];
			
            // Convert the weight position from Joint local space to object space
            glm::vec3 rotPos = joint.orient * weight.pos;
			
            vert.pos += ( joint.pos + rotPos ) * weight.bias;
        }
		
        mesh.positionBuffer.push_back(vert.pos);
        mesh.tex2DBuffer.push_back(vert.tex0);
    }
	
    return true;
}
bool _ModelMD5::prepareNormals(Mesh &mesh){
    mesh.normalBuffer.clear();
	
    // Loop through all triangles and calculate the normal of each triangle
    for ( unsigned int i = 0; i < mesh.tris.size(); ++i )
    {
        glm::vec3 v0 = mesh.verts[ mesh.tris[i].indices[0] ].pos;
        glm::vec3 v1 = mesh.verts[ mesh.tris[i].indices[1] ].pos;
        glm::vec3 v2 = mesh.verts[ mesh.tris[i].indices[2] ].pos;
		
        glm::vec3 normal = glm::cross( v2 - v0, v1 - v0 );
		
        mesh.verts[ mesh.tris[i].indices[0] ].normal += normal;
        mesh.verts[ mesh.tris[i].indices[1] ].normal += normal;
        mesh.verts[ mesh.tris[i].indices[2] ].normal += normal;
    }
	
    // Now normalize all the normals
    for (unsigned int i = 0; i < mesh.verts.size(); i++)
    {
        Vertex& vert = mesh.verts[i];
		
        glm::vec3 normal = glm::normalize( vert.normal );
        mesh.normalBuffer.push_back(normal);
		
        // Reset the normal to calculate the bind-pose normal in joint space
        vert.normal = glm::vec3(0);
		
        // Put the bind-pose normal into joint-local space
        // so the animated normal can be computed faster later
        for ( int j = 0; j < vert.weightCount; ++j )
        {
            const Weight& weight = mesh.weights[vert.startWeight + j];
            const Joint& joint = joints[weight.jointID];
            vert.normal += (normal * joint.orient) * weight.bias;
        }
    }
	
    return true;
}
void _ModelMD5::draw(int &index,double scale){
    glPushMatrix();
    glMultMatrixf( glm::value_ptr(localToWorldMatrix) );
	
    // Render the meshes
    for (unsigned int i = 0; i < meshes.size(); i++)
    {
        renderMesh(meshes[i]);
    }
	
    //animation.draw();
	
    /*for (unsigned int i = 0; i < meshes.size(); i++)
    {
        renderNormals(meshes[i]);
    }*/
	
    glPopMatrix();
}
void _ModelMD5::renderMesh(const Mesh &mesh){
    glColor3f( 1.0f, 1.0f, 1.0f );
    glEnableClientState( GL_VERTEX_ARRAY );
    glEnableClientState( GL_TEXTURE_COORD_ARRAY );
    glEnableClientState( GL_NORMAL_ARRAY );
	
    glBindTexture( GL_TEXTURE_2D, mesh.texID );
    glVertexPointer( 3, GL_FLOAT, 0, &(mesh.positionBuffer[0]) );
    glNormalPointer( GL_FLOAT, 0, &(mesh.normalBuffer[0]) );
    glTexCoordPointer( 2, GL_FLOAT, 0, &(mesh.tex2DBuffer[0]) );
	
    glDrawElements( GL_TRIANGLES, (int)mesh.indexBuffer.size(), GL_UNSIGNED_INT, &(mesh.indexBuffer[0]) );
	
    glDisableClientState( GL_NORMAL_ARRAY );
    glDisableClientState( GL_TEXTURE_COORD_ARRAY );
    glDisableClientState( GL_VERTEX_ARRAY );
	
    glBindTexture( GL_TEXTURE_2D, 0 );
}
void _ModelMD5::renderNormals(const Mesh& mesh){
	
    glPushAttrib( GL_ENABLE_BIT );
    glDisable( GL_LIGHTING );
	
    glColor3f( 1.0f, 1.0f, 0.0f );// Yellow
	
    glBegin( GL_LINES );
    {
        for ( unsigned int i = 0; i < mesh.positionBuffer.size(); ++i )
        {
            glm::vec3 p0 = mesh.positionBuffer[i];
            glm::vec3 p1 = ( mesh.positionBuffer[i] + mesh.normalBuffer[i] );
			
            glVertex3fv( glm::value_ptr(p0) );
            glVertex3fv( glm::value_ptr(p1) );
        }
    }
    glEnd();
	
    glPopAttrib();
}
bool _ModelMD5::prepareMesh(Mesh &mesh, const _MD5Animation::FrameSkeleton& skel){
    for (unsigned int i = 0; i < mesh.verts.size(); i++)
    {
        const Vertex& vert = mesh.verts[i];
        glm::vec3& pos = mesh.positionBuffer[i];
        glm::vec3& normal = mesh.normalBuffer[i];
		
        pos = glm::vec3(0);
        normal = glm::vec3(0);
		
        for (int j = 0; j < vert.weightCount; j++)
        {
            const Weight& weight = mesh.weights[vert.startWeight + j];
            const _MD5Animation::SkeletonJoint& joint = skel.joints[weight.jointID];
			
            glm::vec3 rotPos = joint.orient * weight.pos;
            pos += ( joint.pos + rotPos ) * weight.bias;
			
            normal += ( joint.orient * vert.normal ) * weight.bias;
        }
    }
    return true;
}
bool _ModelMD5::checkAnimation(const _MD5Animation& animation) const{
    if (iNumJoints != animation.getNumJoints())
    {
        return false;
    }
	
    // Check to make sure the joints match up
    for (unsigned int i = 0; i < joints.size(); i++)
    {
        const Joint& meshJoint = joints[i];
        const _MD5Animation::JointInfo& animJoint = animation.getJointInfo(i);
        if (meshJoint.name != meshJoint.name){return false;}
		if (meshJoint.parentID != animJoint.parentID ){return false;}
    }
	
    return true;
}
void _ModelMD5::removeQuotes(std::string &str){
    size_t n;
    while ( ( n = str.find('\"') ) != std::string::npos ) str.erase(n,1);
}
int _ModelMD5::getFileLength(std::istream &file){
    int pos = (int)file.tellg();
    file.seekg(0, std::ios::end );
    int length = (int)file.tellg();
    // Restore the position of the get pointer
    file.seekg(pos);
	
    return length;
}
void _ModelMD5::ignoreLine(std::istream &file, int length){
    file.ignore( length, '\n' );
}
_ModelMD5::ElapsedTime::ElapsedTime( float maxTimeStep /* = 0.03333f */ )
: fMaxTimeStep( maxTimeStep )
, fPrevious ( std::clock() / (float)CLOCKS_PER_SEC ){}
float _ModelMD5::ElapsedTime::getElapsedTime() const{
    float fCurrentTime = std::clock() / (float)CLOCKS_PER_SEC;
    float fDeltaTime = fCurrentTime - fPrevious;
    fPrevious = fCurrentTime;
	
    // Clamp to the max time step
    fDeltaTime = std::min( fDeltaTime, fMaxTimeStep );
	
    return fDeltaTime;
}